package com.example.mq.mqserver.datacenter;

import com.example.mq.common.BinaryTool;
import com.example.mq.common.MqException;
import com.example.mq.mqserver.core.MSGQueue;
import com.example.mq.mqserver.core.Message;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

@Slf4j
//对硬盘上的message进行管理
//规定每个队列都对应一个目录,
//每个目录下都包含两个文件:queue_data.txt, queue_state.txt,分别用来存储数据和统计数据个数
//
public class MessageFileManager {
    public void init() {
        //为了接口的统一性
        //暂时不处理,后续可自行添加
    }

    //表示该队列下统计的文件信息
    public static class State{
        public int totalCount = 0; //总消息数量
        public int validCount = 0; //有效消息数量
    }

    //获取消息所处队列的目录
    private String getQueueDir(String queueName){
        return "./data/" + queueName;
    }

    //获取消息所处队列的数据文件路径
    private String getQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    //获取消息所处队列的统计文件路径
    private String getQueueStatePath(String queueName){
        return getQueueDir(queueName) + "/queue_state.txt";
    }

    //创建队列对应的目录和文件
    public void createQueueFiles(String queueName) throws IOException {
        //1.创建队列对应目录
        File directory = new File(getQueueDir(queueName));
        if(!directory.exists()){
            boolean success = directory.mkdirs();
            if(!success){
                throw new IOException("[MessageFileManager] 创建目录失败" + directory.getAbsoluteFile());
            }
        }
        //2.创建队列对应数据文件
        File dataFile = new File(getQueueDataPath(queueName));
        if(!dataFile.exists()){
            boolean success = dataFile.createNewFile();
            if(!success){
                throw new IOException("[ManagerFileManager] 创建文件失败" + dataFile.getAbsoluteFile());
            }
        }
        //3.创建队列对应统计文件
        File stateFile = new File(getQueueStatePath(queueName));
        if(!stateFile.exists()){
            boolean success = stateFile.createNewFile();
            if(!success){
                throw new IOException("[ManagerFileManager] 创建文件失败" + stateFile.getAbsoluteFile());
            }
        }
        //4.初始化队列文件
        State state = new State();
        writeState(queueName, state);
    }

    //删除队列对应的目录和文件
    public void destroyQueueFiles(String queueName) throws IOException {
        //要删除目录需要先删除里面的文件
        File dataFile = new File(getQueueDataPath(queueName));
        boolean success1 = dataFile.delete();
        File stateFile = new File(getQueueStatePath(queueName));
        boolean success2 = stateFile.delete();
        File directory = new File(getQueueDir(queueName));
        boolean success3 = directory.delete();
        //确保文件都删除了
        if(!success1 || !success2 || !success3){
            throw new IOException("[MessageFileManager] 删除队列文件失败" + dataFile.getAbsoluteFile());
        }
    }

    //从文件中读取统计信息
    private State readState(String queueName){
        //state文件是文本文件,可以使用Scanner来读写
        State state = new State();
        try(InputStream inputStream = new FileInputStream(getQueueStatePath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            state.totalCount = scanner.nextInt();
            state.validCount = scanner.nextInt();
            return state;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //向文件中写入统计信息
    private void writeState(String queueName, State state){
        //OutputStream默认情况打开文件,会清空文件
        try(OutputStream outputStream = new FileOutputStream(getQueueStatePath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.print(state.totalCount + "\t" + state.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //判断队列对应的数据文件和统计文件都存在
    private boolean checkQueueFilesExist(String name) {
        //如果两个文件都存在了,则说明目录也存在,不需要额外判断
        File dataFile = new File(getQueueDataPath(name));
        File stateFile = new File(getQueueStatePath(name));
        if(!dataFile.exists() || !stateFile.exists()){
            return false;
        }
        return true;
    }

    //将消息写入对应的队列文件
    public void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
        //1.判断对应的队列文件是否存在
        if(!checkQueueFilesExist(queue.getName())){
            throw new MqException("[MessageFileManager] 队列文件不存在 queueName:" + queue.getName());
        }
        //2.将消息转成二进制
        synchronized (queue){
            byte[] data = BinaryTool.toBytes(message);
            //3.获取到文件末尾位置,设置文件位置的偏移量
            File dataFile = new File(getQueueDataPath(queue.getName()));
            //每条消息前4个字节用来表示消息的长度
            message.setOffsetBeg(dataFile.length() + 4);
            message.setOffsetEnd(dataFile.length() + 4 + data.length);
            //4.写入消息文件
            try(OutputStream outputStream = new FileOutputStream(dataFile, true)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    //写入消息的长度(4个字节)
                    dataOutputStream.writeInt(data.length);
                    //写入消息本体
                    dataOutputStream.write(data);
                }
            }
            //5.更新统计文件
            State state = readState(queue.getName());
            state.totalCount += 1;
            state.validCount += 1;
            writeState(queue.getName(), state);
        }
    }

    //从硬盘上删除消息(逻辑删除)
    //此处传的消息必须为有效消息
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
        //1.判断队列对应的文件是否存在
        if(!checkQueueFilesExist(queue.getName())){
            throw new MqException("[MessageFileManager] 队列文件不存在 queueName:" + queue.getName());
        }
        synchronized (queue){
            //2.从文件中获取到消息
            byte[] data = new byte[(int)message.getOffsetEnd() - (int)message.getOffsetBeg()];
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")){
                //移动文件光标
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(data);
                //3.将消息反序列化并标记为无效
                Message diskMessage = (Message)BinaryTool.fromBytes(data);
                diskMessage.setIsValid((byte)0x00);
                //4.将消息重新写入文件
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(BinaryTool.toBytes(diskMessage));
            }
            //5.更新统计文件
            State state = readState(queue.getName());
            //逻辑删除,不需要修改totalCount
            state.validCount -= 1;
            writeState(queue.getName(), state);
        }
    }

    //从队列文件中读取所有 有效消息
    //在程序启动时,进行调用,读取文件中的所有有效消息,加载到内存中
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                //记录当前文件光标,方便赋值Message中offsetBeg和offsetEnd值
                long currentOffset = 0;
                while(true){
                    //1.读取当前消息的长度
                    //当 readInt 读取到文件末尾的时候,会抛出异常 EOFException
                    int messageSize = dataInputStream.readInt();
                    //2.根据读取到的长度,在文件中读取
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if(messageSize != actualSize){
                        throw new MqException("[MessageFileManager] 文件读取错误 queueName:" + queueName);
                    }
                    //3.将字节数组反序列化成对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    //4.判断当前消息是否为有效消息
                    if(message.getIsValid() == 0x00){
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    //5.计算在文件中偏移量并赋值,将消息添加到链表中
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }
            } catch (EOFException e){
                //文件读取到末尾,并非真的异常
                log.info("[MessageFileManager] 文件读取完毕");
            }
        }
        return messages;
    }

    //获取新的队列数据文件
    private String getQueueNewDataPath(String queueName){
        return getQueueDir(queueName) + "/new_queued_data.txt";
    }

    //约定 当文件中总消息数超过 1000 并且 无效消息的个数 超过一半 就进行一次gc
    public boolean checkGC(String queueName){
        State state = readState(queueName);
        if(state.totalCount > 1000 && (double)state.validCount / (double) state.totalCount > 0.5){
            return true;
        }
        return false;
    }

    //对消息文件进行gc操作
    //此处使用复制算法的方式进行
    public void gc(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
        //当一个线程进行GC的时候,其他线程不允许进行操作
        synchronized (queue){
            //由于gc操作比较耗时,在这输出一下gc的时间
            //计算gc开始时间
            long gcBeg = System.currentTimeMillis();
            //1.创建新文件
            File newDataFile = new File(getQueueNewDataPath(queue.getName()));
            if(newDataFile.exists()){
                log.info("[MessageFileManager] 存在复制的目标文件");
                newDataFile.delete();
            }
            if(!newDataFile.createNewFile()){
                throw new MqException("[MessageFileManager] 复制文件创建失败,queueName:" + queue.getName());
            }
            //2.从旧文件中读取消息
            //此处读取到的信息都是有效信息
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
            //3.将有效消息写入新文件
            try(OutputStream outputStream = new FileOutputStream(newDataFile)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        //不调用sendMessage,防止outputStream频繁关闭影响性能
                        byte[] buffer = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            //4.让新文件替换就旧文件
            File oldDataFile = new File(getQueueDataPath(queue.getName()));
            if(!oldDataFile.delete()){
                throw new MqException("[MessageFileManager] 旧数据文件删除失败 oldDataFile:" + oldDataFile.getAbsoluteFile());
            }
            if(!newDataFile.renameTo(oldDataFile)){
                throw new MqException("[MessageFileManager] 文件重命名失败 newDataFile:" + newDataFile.getAbsoluteFile());
            }
            //5.更新统计信息的文件
            State state = readState(queue.getName());
            state.totalCount = state.validCount;
            writeState(queue.getName(), state);

            //计算gc结束时间
            long gcEnd = System.currentTimeMillis();
            log.info("[MessageFileManager] " + queue.getName() + "gc完成, 总耗时: " + (gcEnd - gcBeg) + "ms");
        }
    }
}
